International Collegiate Programming Contest · 学习系统

Hello, ACM-ICPC
国际大学生程序设计竞赛

ACM-ICPC 起源于 1970 年,由 ACM(国际计算机学会)主办,是历史最悠久、影响力最大的大学生程序设计竞赛。 这不是考试,而是一场团队竞技3 人一队、共用一台电脑、5 小时, 解决 7~13 道英文算法题。本页按 「认知 → 语言 → 算法 → 实战」的递进路线组织, 帮助零基础的你一步步走上区域赛领奖台。

队伍人数
3人 / 队
比赛时长
5小时
题目数量
7–13道 · 英文
真题覆盖
10年 · 2016–2026

排名规则:过题数量优先,数量相同比总用时;每次错误提交罚时 20 分钟,未通过的题目不计时。 配套三大学习网站:Codeforces · 洛谷 · OI Wiki(详见第 11 章)。

01OJ 与评测系统01 / 13
01

OJ 与评测系统

新手第一课 · 必懂

Online Judge(OJ)是为程序设计竞赛而生的自动判题系统:提交源代码(C/C++/Java/Python 等), 系统编译执行,用出题人预设的测试数据判断正确性,同时有严格的时间与内存限制。

一道 OJ 题目的结构

题号 · 标题
时间限制 · 内存限制
题意描述(Input / Output 格式)
输入样例 · 输出样例
样例解释 / Hint

评测结果的 7 种情况(必背)

徽章全称含义
ACAccepted题目通过 ✓ 恭喜!
WAWrong Answer答案错误,全部或部分输入没有得到预期输出
RTERun Time Error运行出错、意外终止——常见于数组越界、除 0、爆栈
TLETime Limit Exceeded运行超时——多半是算法复杂度不对,先想复杂度再改代码
PEPresentation Error输出格式错(多了空格、少了换行)——距离 AC 只差一步
MLEMemory Limit Exceeded内存溢出
CECompile Error编译错误——本地先编译一遍再提交

Codeforces(CF)生态速览

  • Pretest → System Test:赛中只过部分测试点(Pretest Passed),赛后统一全量测评,没过就是 FST
  • Hack 机制:Lock 自己的代码后可看同 Room 选手代码,构造数据卡掉对方:成功 +100,失败 −50
  • GYM:还原真实 ACM-ICPC 规则的训练场,可组队虚拟参赛(Virtual Participation)
  • Problemset:按标签(dp / graphs / dsu…)和难度筛选题目,适合专题训练
Rating 区间头衔颜色
[2600, ∞)International Grandmaster
[2200, 2600)Grandmaster
[2050, 2200)International Master
[1900, 2050)Master
[1700, 1900)Candidate Master
[1500, 1700)Expert
[1350, 1500)Specialist绿
[1200, 1350)Pupil绿
(−∞, 1200)Newbie
必背:新用户初始 Rating 为 1500(灰名);Rating 高于 1700 参加.Div.1, 低于则参加.Div.2——新手从 Div.2 打起完全正常,不要有心理负担。
02语言·复杂度02 / 13
02

语言基础与复杂度

阶段一 · ★★★★★

竞赛主流语言是 C++。先掌握语法与 STL,再建立「先估复杂度、再写代码」的习惯——这是所有算法的地基。

竞赛通用模板(背下来)

C++#include <bits/stdc++.h>     // 万能头(绝大多数 OJ 支持)
using namespace std;

int main() {
    ios::sync_with_stdio(false);   // 关闭同步,cin/cout 提速
    cin.tie(nullptr);
    int n;
    cin >> n;
    vector<int> a(n);
    for (auto &x : a) cin >> x;
    sort(a.begin(), a.end());
    cout << *max_element(a.begin(), a.end()) << "\n";
    return 0;
}
多组数据是竞赛题的常态:while (scanf("%d", &n) != EOF)while (~scanf("%d", &n))——EOF 即 −1,取反后为 0 使循环退出。

STL 必会清单

  • vector 动态数组 / sort 排序
  • pair 二元组 / map · set(红黑树)
  • priority_queue 堆 / queue · stack
  • string / lower_bound · upper_bound
  • __builtin_popcount 二进制 1 的个数
  • 注意:map 有序、复杂度 O(log n)

时间复杂度速查(1 秒 ≈ 108 次运算)

n 的规模可行复杂度典型算法
n ≤ 12O(n!)全排列暴力搜索
n ≤ 25O(2n)子集枚举、状压搜索
n ≤ 5000O(n²)朴素 DP、双重循环
n ≤ 105O(n log n)排序、二分、线段树
n ≤ 107O(n)线性扫描、双指针
n ≥ 109O(log n) / O(1)数学公式、快速幂

部分排序算法复杂度(经典表)

排序方法最好时间平均时间最坏时间辅助空间稳定性
直接插入O(n)O(n²)O(n²)O(1)稳定
二分插入O(n)O(n²)O(n²)O(1)稳定
希尔O(n1.25)O(1)不稳定
冒泡O(n)O(n²)O(n²)O(1)稳定
快速O(n log n)O(n log n)O(n²)O(log n)不稳定
直接选择O(n²)O(n²)O(n²)O(1)不稳定
O(n log n)O(n log n)O(n log n)不稳定

亲手试一试 · 复杂度体检器

 LIVE DEMO — 1 秒 ≈ 108 次运算,你的算法跑得动吗?
n =

竞赛解题第一步:先看数据范围,再定算法复杂度。看到 n ≤ 10⁶ 还想写双重循环?TLE 会教育你的。

03递进式学习路线03 / 13
03

递进式学习路线

本站核心 · 按图索骥

学习算法是一个循序渐进的过程。不要指望一口气啃完《算法导论》再做题——看书与刷题应当是螺旋式推进的。 下面是为零基础新手设计的四阶段路线,每阶段都有明确目标。

阶段一 语言基础
↓ 约 2 周
阶段二 入门算法
↓ 约 4 周
阶段三 核心算法
↓ 约 8 周
阶段四 专题进阶
阶段学什么练什么达成标志
一 语言基础
~2 周
C++ 语法、STL 容器、调试技巧模拟 / 水题 50 道以上能独立 AC 简单题
二 入门算法
~4 周
枚举、模拟、排序、二分、贪心、简单数学CF Div.2 A/B、洛谷入门组现场赛第一题稳定拿下
三 核心算法
~8 周
搜索、DP、基础数据结构、图论CF Div.2 C/D、往届区域赛真题区域赛铜牌区
四 专题进阶
持续
网络流、数论、字符串、计算几何、树上问题真题套题 + 虚拟参赛 + 补题银牌 / 金牌区

螺旋式学习法(看书 ↔ 刷题)

看书学习算法原理
↻ 配合刷题巩固
刷题积累经验
↻ 遇到新算法主动学
递进三原则:① 绝不做一眼就能看出答案的题——做那种题无论多少次都没有实质提升; ② 做比现阶段水平略高但够得着的题;③ 没做出的题一定要补,不留知识漏洞。

一周计划示例(参考小明的故事)

  • 周一 ~ 周五:每天固定时间做 1 道题(工作日做题)
  • 周六 ~ 周日:看《算法竞赛入门经典》等书,梳理本周知识点
  • 组队后:每周一场组队训练(区域赛真题虚拟参赛)+ 赛后讲题补题
  • 执行策略:队友之间互相监督打卡——「你今天做题了吗?没有?还不赶紧去做!」
04模拟与枚举04 / 13
04

模拟与枚举

阶段二 · 入门必经

模拟题是按照题目要求操作即可得到结果的一类题目,注重考查代码实现能力、细节和特殊情况处理。 区域赛第一题(签水题)几乎都是模拟。

实战示范:股票交易(入门经典题)

题面 · 样例

题意:给定连续时间点的股价数据,寻找一次买卖股票的每股最大收益(先买后卖);无论如何无法取得收益则输出 No solution

输入:多组测试数据(约 10 组),以 EOF 结尾。第一行为 n(0 < n ≤ 1 000 000);第二行为 n 个 int 范围内的正整数。

样例:5 / 1 2 3 4 2 → 3(第 1 分钟 1 买、第 4 分钟 4 卖);2 / 2 2 → No solution

三种解法的演进(思考过程比答案更重要)
方法思路复杂度结论
暴力求解枚举每对买/卖日期组合,取最大收益Ω(n²)n = 10⁶ 时必超时,不可行
分治转为「最大子数组」问题:递归左半、右半、跨越中点三种情况O(n log n)可行,代码较长
线性扫描从前往后扫,维护前 i−1 项最小值 x,ans = max(ans, a[i] − x)O(n)最优解,代码极短
C++int main() {
    int n;
    while (~scanf("%d", &n)) {          /* 多组数据,EOF 结尾 */
        int res, ans = 0, x;
        scanf("%d", &x);  res = x;       /* res 记录前缀最小值 */
        for (int i = 2; i <= n; i++) {
            scanf("%d", &x);
            if (ans < x - res) ans = x - res;  /* 更新最大收益 */
            if (x < res) res = x;              /* 更新最小值   */
        }
        if (ans == 0) printf("No solution\n");
        else printf("%d\n", ans);
    }
    return 0;
}

经典陷阱:数据溢出(2017 沈阳站 I 题 Little Boxes)

C++/* a,b,c,d ≤ 2^62,四数之和最大可达 2^64,unsigned long long 也装不下 */
if (a == (1ULL << 62) && b == (1ULL << 62) &&
    c == (1ULL << 62) && d == (1ULL << 62))
    printf("18446744073709551616\n");   /* 特判输出 2^64 的真实值 */
else
    printf("%llu\n", a + b + c + d);
模拟题三大坑:① 数据范围溢出(int → long long → unsigned long long 要敏感); ② 输出格式细节(空格、换行、Case 编号);③ 多组数据要清空用过的容器
05搜索 DFS / BFS05 / 13
05

搜索

阶段三 · 核心算法

搜索用于枚举所有情况或遍历图中每个点,适用于数据范围较小的场景。核心两板斧: 深度优先搜索(DFS,递归实现)广度优先搜索(BFS,借助队列,求最少步数)

DFS / BFS 对比与模板

DFS 深度优先BFS 广度优先
实现递归 / 栈队列
适合枚举所有方案、连通性、树上问题最少步数、层序扩展
去重vis 数组 / set 记录访问状态入队时立即标记
C++/* DFS:递归遍历 */
void dfs(int u) {
    vis[u] = true;
    for (int v : g[u]) if (!vis[v]) dfs(v);
}

/* BFS:队列 + 最少步数 */
queue<int> q;  q.push(s);  vis[s] = true;
while (!q.empty()) {
    int u = q.front();  q.pop();
    for (int v : g[u]) if (!vis[v]) {
        vis[v] = true;  step[v] = step[u] + 1;  q.push(v);
    }
}

剪枝:搜索不超时的关键

  • 可行性剪枝:到达某状态时,无论后面怎么做都达不到题目条件,立即回溯
  • 最优性剪枝:若当前可能的最高得分都达不到已有最优解,直接停止搜索
  • 状态保存与恢复:回溯前先备份地图/状态,递归返回后恢复——小心全局变量被污染
例题:Counting Cliques(2016 沈阳站 E)· 统计大小为 S 的团

思路:数据范围小(N ≤ 100,M ≤ 1000,S ≤ 10),从每个点 DFS。加一个新点前先检查它与当前团内所有点都有边;搜完一个点要把它重新标记为未使用,避免遗漏。

技巧:邻接表只加单向边避免同一个团被重复统计;同时用邻接矩阵 e[u][v] 实现 O(1) 判边。多组数据记得清零。

经验:搜索题先算状态数,估计剪枝后能否在时限内跑完;样例较长时反复核对样例输入有没有敲错——查半天代码发现是样例敲错,非常吃亏。
06动态规划06 / 13
06

动态规划

阶段三 · 核心算法

动态规划(DP)是一种决策过程:把多阶段问题拆成很多单阶段,利用阶段之间的关系逐个求解。 重点在于找到状态转移方程——可能从前向后、区间从小到大、沿轮廓线、或跟数位相关,变化多端。

经典模型清单(按序攻克)

  • 线性 DP:LIS 最长上升子序列、LCS
  • 背包:01 / 完全 / 多重
  • 区间 DP:石子合并类
  • 树形 DP / 数位 DP(记忆化搜索)
  • 轮廓线 DP(状态压缩)
  • 递推 vs 记忆化搜索两种写法

区间 DP 示例:石子合并(朴素版)

C++/* dp[i][j] = 合并区间 [i, j] 的最小代价;大区间由小区间推出 */
for (int len = 2; len <= n; len++)
    for (int i = 1; i + len - 1 <= n; i++) {
        int j = i + len - 1;
        dp[i][j] = INF;
        for (int k = i; k < j; k++)
            dp[i][j] = min(dp[i][j],
                dp[i][k] + dp[k + 1][j] + sum(i, j));
    }

真题状态设计对照

真题DP 类型状态定义
Pangu and Stones(2017 北京 J)区间 DPdp[i][j][k]:区间 [i, j] 分成 k 堆的最小代价;每次只能合并连续的 L~R 堆,答案 dp[1][N][1]
划分型 DP(2021 回忆版改编)二分答案 + 贪心判定答案单调 → 二分 x,判定「能否分成 ≤ m 段且每段和 ≤ x」
TSP(2024 题库精选改编)状压 DPdp[S][i]:已访问集合 S、当前在 i 的最短路径,O(n²·2ⁿ)
总结:处理区间时注意边界与初始化(不可达初始化为大数);DP 代码一般不长,但要注重思维和逻辑的严谨性——想到、想清楚,代码写起来就容易了。
07数据结构07 / 13
07

数据结构

阶段三 · 核心算法

当暴力求解复杂度偏高时,可根据问题中的限定(如区间、前缀、最大异或)使用相应数据结构维护相应的值,达到优化时间复杂度的目的。

常用数据结构速查

结构典型用途复杂度
栈 / 队列括号匹配、模拟过程O(1) 单步
优先队列(堆)按时间取最小/最大事件、哈夫曼式合并O(log n)
并查集连通性、合并集合近似 O(α)
树状数组单点修改 + 区间求和、逆序对、离线查询O(log n)
线段树区间修改(lazy)+ 区间最值/求和、扫描线O(log n)
字典树 Trie字符串前缀、01 字典树求最大异或O(位数)

树状数组模板(lowbit 是灵魂)

C++int lowbit(int x) { return x & (-x); }

void update(int x, int d) {
    while (x <= n) { BIT[x] += d;  x += lowbit(x); }
}
int query(int x) {
    int ret = 0;
    while (x) { ret += BIT[x];  x -= lowbit(x); }
    return ret;
}
/* 注意:树状数组下标从 1 开始,不能出现 0,否则 lowbit 会出问题 */

01 字典树求最大异或(经典应用)

C++/* 把每个数按二进制从高位到低位插入字典树;
   查询与 x 异或最大的值:每一位尽量走【相反】的子节点,
   两个数该位不同,异或结果才为 1 */
int query(int x) {
    int now = 0, t, ret = 0;
    for (int i = 30; i >= 0; i--) {
        t = (x >> i) & 1;
        if (nxt[now][t^1] && cnt[nxt[now][t^1]]) {
            ret |= (1 << i);          /* 这一位异或结果为 1 */
            now = nxt[now][t^1];
        }
        else now = nxt[now][t];
    }
    return ret;
}
/* 需要支持删除时:每个节点记录经过次数 cnt,
   插入 +1、删除 −1,cnt 为 0 的节点视作不存在 */
要点:字典树也可以高效解决「求位运算最值」类问题——01 字典树 + 支持删除是区域赛数据结构题的常见组合。
08图论08 / 13
08

图论

阶段三~四 · ★★★★★

图论内容很多:最短路、生成树、匹配与网络流、拓扑结构、连通性、强连通分量,甚至还有给条件构造图的构造题。

最短路:堆优化 Dijkstra 模板

C++typedef pair<long long, int> pli;
priority_queue<pli, vector<pli>, greater<pli>> pq;   /* 小根堆 */

void dijkstra(int s) {
    fill(d + 1, d + n + 1, INF);
    d[s] = 0;  pq.push({0, s});
    while (!pq.empty()) {
        int u = pq.top().second;  pq.pop();
        for (auto [v, w] : g[u])
            if (d[v] > d[u] + w) {
                d[v] = d[u] + w;
                pq.push({d[v], v});
            }
    }
}
/* 无负权边用 Dijkstra;有负权用 SPFA;点少可用 Floyd */

建图技巧:有时候建出来就会做了

  • 虚点建图(Meeting 类问题):集合内两两连边边太多——把每个集合作一个新点,集合点向成员连权值 t 的有向边,成员向集合点连权值 0 的边,之后各跑一遍最短路即可
  • 拆点(Kejin Game 类问题):把每个点 i 拆成 i 和 i′ 建源汇,原图问题转化为最小割,直接用最大流求解
  • 拓扑排序:Kahn 算法判环(出队数 < n 则有环),也可用于 DAG 上的 DP 与依赖调度
  • 构造题(Graph Reconstruction 类问题):由度数序列构图用 Havel–Hakimi 定理——所有点度数从大到小排序,每次考虑一个点向之后的点连边直至满足度数,再重新排序;度数连不完或出现负数则不可构图
启示:在图论问题中直接连边可能不行,需要新建点或拆点等——建图是很关键的一步,有时建出来就会做了。 另外,有「直接获得和间接获得、有前置条件、求最值」特征的题,都可以往网络流方向考虑。
09数论·数学09 / 13
09

数论与数学

阶段三~四 · 推公式能力

运用数论知识和计算机快速计算的能力,可以解决许多问题。gcd / lcm、快速幂、筛法、容斥是四大件。

基础模板三连

C++/* 最大公约数(辗转相除) */
long long gcd(long long a, long long b) { return b ? gcd(b, a % b) : a; }
/* lcm(a,b) = a / gcd(a,b) * b —— 注意先除后乘防溢出 */

/* 快速幂:O(log b) */
long long qpow(long long a, long long b, long long mod) {
    long long ret = 1;  a %= mod;
    while (b) {
        if (b & 1) ret = ret * a % mod;
        a = a * a % mod;
        b >>= 1;
    }
    return ret;
}

/* 埃氏筛打素数表 */
for (int i = 2; i <= 100000; i++) isprime[i] = true;
for (int i = 2; i <= 100000; i++)
    if (isprime[i])
        for (long long j = 2; i * j <= 100000; j++)
            isprime[i * j] = false;

容斥原理:正着不好求就求反面

求 1~n 中与 n 互质的数的四次方之和这类问题,正着求不好求,改为对 n 分解质因数后枚举质因数集合使用容斥:

C++/* 前提:fac[] 已存入 n 的所有不同质因子 */
for (long long s = 1; s < (1LL << cnt); s++) {
    long long now = 1;  int bits = 0;
    for (long long j = 0; j < cnt; j++)
        if (s >> j & 1) { bits++;  now *= fac[j]; }
    /* now 的倍数(≤n)有 n/now 个,四次方和代入公式 */
    /* 四次方和公式:Σk⁴ = n(n+1)(2n+1)(3n²+3n−1)/30 */
    /* 奇数个质因子做加,偶数个做减;除以 30 用费马小定理求逆元 */
    temp = qpow(now, 4) * (n / now) % mod * (n / now + 1) % mod
         * (2 * n / now + 1) % mod * ((3 * n / now * n / now
         + 3 * n / now + mod - 1) % mod) % mod
         * qpow(30, mod - 2) % mod;     /* qpow(30, mod-2) 即 30 的逆元 */
    if (bits & 1) ans = (ans + temp) % mod;
    else          ans = (ans + mod - temp) % mod;
}
/* 最后:用总和减去不互质的部分,得到互质部分的四次方和 */

数学推导示例:A Simple Math Problem(2016 大连站 D)

已知 X + Y = a,lcm(X, Y) = b,求 X、Y。
设 gcd(X, Y) = g,则 X = g·k₁,Y = g·k₂ 且 k₁、k₂ 互质。方程改写为 k₁ + k₂ = a/g,k₁ · k₂ = b/g——说明 g 也是 a、b 的最大公约数。 于是构造一元二次方程 gx² − ax + b = 0,判别式 Δ = a² − 4gb; Δ < 0、Δ 不是完全平方数、或 (a ± √Δ) 不为偶数时输出 No Solution, 否则 X = (a − q)/2,Y = (a + q)/2。
总结:在 ACM 竞赛中 gcd 和 lcm 出现率非常高,看到相关内容往这方面想、变换条件寻求解法; 式子不好推时结合欧拉函数/积性函数;正面不好想可以想反面,反面有交集时用容斥原理。
10训练方法10 / 13
10

训练方法

锦囊妙计 · 心法

源自北航 ACM 集训队学长的实战经验:入门、刷题、团队三个维度的方法论。

入门:刷题 vs 看书

「只看书」容易望而生畏——《算法导论》全书 745 页,想读完整本再去「捕鱼」,无异于读完字典再去看书; 「只刷题」又会留下知识漏洞。正确姿势是螺旋式:看书时配合刷题巩固知识点, 刷题时遇到没学过的算法主动去学,慢慢积累,会有十分可观的收获。

刷题:数量 vs 质量

  • 不搞题海战术——做大量题而不考虑质量和效率并不可取
  • 遇到不会的题不要丢到一边,积极补题,完全弄明白
  • 不做一眼就会的题,浪费时间且无提升
  • 解题报告:题目分析 → 算法分析(复杂度、是否最优)→ 代码实现
  • 解题报告也应包括已做出的题,加深理解与记忆
  • 参考 Codeforces 大神的 Blog 题解,交流产生灵感

训练:团队 vs 个人

  • 团队的好处:互相监督、互相鼓励、分工学习不同专题,减轻学习负担
  • 但团队配合建立在个人实力之上——努力提高个人实力才是硬道理
  • 三人一台电脑的基本分工:①各自为政式(谁擅长谁上);②指手画脚式(一人敲码、两人观察提议)——最好的策略是两者结合
  • 经典协作流程:A 想思路 → 三人讨论细节 → B 操刀写码、C 在旁 debug,A 去看下一题
  • 注意避免「两个人同时做了同一道题」的低效情况——开赛前明确分工!

赛场心态(来自学长们的血泪)

第一次比赛 WA 十几次很正常;发现知识盲点时慌了没有用——忘掉比赛和排名,专注解题。 第一道题过了,就仿佛回到了训练的时候。友谊第一,比赛第二,每次比赛抱着进步的心态,才能走得长远。
11装备获取·网站11 / 13
11

装备获取

三大网站 × 经典书籍

工欲善其事,必先利其器。训练平台与经典书籍是 ACMer 的两件核心装备——其中下面三个网站,建议立刻收藏,几乎覆盖从入门到区域赛的全部资源。

三大必收藏学习网站(新手首选)

网站网址定位新手怎么用
Codeforces codeforces.com 全球最高频的算法竞赛平台(俄罗斯萨拉托夫国立大学团队维护) 每周多场比赛、Rating 晋级体系;从 Div.2 打起练思维;赛后补题并阅读公开的优胜代码;GYM 组队模拟区域赛
洛谷 luogu.com.cn 中文最大的算法竞赛社区与 OJ 之一 题面中文友好;跟官方题单 / 知识点标签循序渐进;递进路线阶段一、二的主战场
OI Wiki oi-wiki.org 中文开源算法竞赛知识库(GitHub 协作维护) 当作「在线字典」:学到哪个专题就查对应章节的系统讲解 + 模板代码 + 推荐习题,查漏补缺
推荐学习闭环:洛谷打基础(题单跟走)→ Codeforces 打比赛(Div.2 + 补题)→ OI Wiki 查理论(讲解 + 模板)→ 回到平台补题巩固。 三站配合,正好对应本站第 03 章的递进式四阶段路线。
其他优质资源(进阶补充)
  • AtCoder(atcoder.jp)——日本平台,ABC 每周一场,题目梯度极适合新手
  • 牛客(nowcoder.com)——国内比赛 + 企业笔试题库,求职向友好
  • Virtual Judge(vjudge.net)——聚合全球各 OJ 题目,方便组队刷题榜
  • LeetCode——自带详细官方题解,按 Easy/Medium/Hard 标注,找工作向

三本经典书(怎么读最有效)

《算法竞赛入门经典》(第 2 版)· 刘汝佳

最大的特点是比较基础,理论讲解少、实践演示多。示例代码十分规范简洁,还包含很多开发、测试和调试的技巧(这在算法书中很少见)。 更适合作为练习指导,配合《算法导论》等算法书使用更佳。

《挑战程序设计竞赛》(第 2 版)· 秋叶拓哉 等著

作者是国际知名选手(World Finals 冠军、Google Code Jam 前十),内容结构优秀、循序渐进, 分为准备篇、初级篇、中级篇、高级篇四篇,对每块基础算法逐一攻克,大部分题目附有实例代码与思路说明。 学长评价:把这本书刷完,你已经是金牌水平了。

《算法导论》(Introduction to Algorithms)· CLRS

全面(745 页)、独立性强(各章自成体系,可按需跳读)、浅显易懂不失严谨(含证明与伪代码)。 不要指望一口气读完——把它当工具书,学某个专题时翻对应章节;网上(知乎、CSDN、博客)有大量大佬的学习笔记可以参考。

广义 ACM:值得一并参加的比赛

  • CCPC 中国大学生程序设计竞赛——国内另一大赛事体系,赛程与 ICPC 互补
  • 蓝桥杯(C/C++、Java 组)——个人赛,入门友好,省赛国赛两级
  • 百度之星、编程之美、Topcoder——大厂 / 平台举办,练手 + 简历加分
选题心法:刷题优先选 CF 上 AC 人数适中(如 500 人左右)、难度略高于自己水平的题;专题训练时用 Problemset 标签筛选,集中攻克薄弱板块。
12权衡与建议12 / 13
12

权衡与建议

值不值得打 ACM?

了解竞赛之后,你还需要权衡参与它能带来什么、会让你失去什么,再根据自己的目标做出选择。

利 vs 弊(正反方观点提炼)

打下坚实的算法基础,大幅提升代码能力、逻辑思维、Debug 心态耗费大量时间,可能挤占做项目、积累工程经验的时间
获奖充实简历;保研加分;IT 企业笔试机试题型与 ICPC 高度相似面试中可能反被提高难度;奖项本身不保证一切
结识大量优秀同行,开阔视野,获得内推等圈子资源不搞 ACM 也能通过实习、项目结识良师益友,各有所得
努力曲线:区域赛银牌/铜牌 ≈ 期末考八九十分——认真学、努力训练并不算太难; 但想拿金牌、进 World Final ≈ 考近满分——成绩越高,相同提升需要的努力越多,需要非常非常多的投入。

五类人群,五条建议

  • 学院派:帮助不大——多读论文、去实验室,比竞赛更有用
  • 保研党:先保证成绩过硬;获奖是锦上添花
  • 考研党:极其耗时,不推荐;机试题型虽相似但难度低很多
  • 出国党:在 GPA 等条件保证下可以搞,锦上添花
  • 实习党:锻炼思维与代码能力,但会缺项目经历——自己权衡
  • 竞赛爱好者:挑战性极强(WF 至今无人全 AC),非常值得拼搏

Q & A 精选

如何平衡学业和 ACM?

课上高效吸收知识,课后专注 ACM(训练、刷题、补题);考期在 ACM 上少投入、专注准备考试。 如果目标极好成绩则必然影响学业,抱着重在参与的心态则很容易平衡——结合自身能力与环境考量。

零基础能打 ACM 吗?

可以。多位受访者都是零基础入坑:北航某学长入学时零基础,研一时拿到 World Final 冠军。 「当你觉得为时已晚,恰恰是最早的时候。」——只要你肯开始,一切都来得及。

什么样的人适合走 ACM 这条路?

当然是喜欢的了:第一,认识到竞赛的好处并想得到它;第二,积极努力;第三,深入思考、权衡利弊。

最后:参赛者在比赛中倾注的那种纯粹的热爱,才是参与 ACM-ICPC 这段经历中最迷人的地方。 一个人空有一腔热爱并无意义——如果你热爱 ACM-ICPC,就请付出和你的热爱等同的努力和坚持。
13真题精讲13 / 13
13

真题详解 · 2016–2026

点击选项即判分

收录 2016–2019 年亚洲区域赛真题(公开题解整理)、2020–2024 年考生回忆版改编题、 2025 年官方样题风格题,以及依据近年赛制编写的 2026 年模拟预测题。 回忆版与改编题可能与原题表述略有出入。选择题点击选项立即判分;思路题点击「查看解析」展开——建议先自己想 10 分钟再看答案。

已作答 0 题 | 答对 0
基础赛事规则必懂常识
ACM-ICPC 现场赛中,每支队伍最多由几名队员组成、共用几台电脑?
答案 B。每队最多 3 名队员,共用一台电脑,比赛持续 5 小时。资源受限正是团队分工与沟通能力的考验来源。
基础赛事规则必懂常识
每次提交不通过,都会在总时间上加上罚时。罚时为?
答案 C。每次提交不通过罚时 20 分钟。过题的正确率也是衡量选手水平的重要指标——ACM 里每一次 WA 都有代价。
基础OJ 系统必懂常识
OJ 评测结果中,RTE(Run Time Error)表示?
答案 C。常见原因:数组越界、除以 0、递归爆栈。输出格式错是 PE,超时是 TLE,编译不过才是 CE——七个结果都要分清。
2016赛事常识真题精选
2016 年 ACM-ICPC 亚洲区域赛中国区共设大连、沈阳、香港、青岛、北京等赛区,当年的 China-Final 在哪所大学举办?
答案 C。2016 年 China-Final 于 12 月 10—11 日在上海大学举办。了解赛区构成有助于规划每年的参赛路线:网络预选赛 → 区域赛现场赛 → China-Final → World Final。
2016单项选择沈阳站 E · Counting Cliques
统计图中大小为 S 的团。存图时邻接表只加单向边的原因是?
答案 B。团是无向概念,若邻接表双向都加,每个团会被枚举多次。技巧总结:邻接表加单向边防重复 + 邻接矩阵 O(1) 判边(新点须与团内所有点相连才加入)+ 搜完把点重新标记为未使用避免遗漏。
2016思路题沈阳站 E · Counting Cliques
N ≤ 100、M ≤ 1000、S ≤ 10 且最大度数 ≤ 20 的图中,统计大小为 S 的团的数量。请给出搜索思路。
解析:① 从每个点出发 DFS,维护当前团内的点集 clique[] 与大小 sz;② 尝试加入新点 x 前,遍历团内所有点 j,用邻接矩阵检查 e[x][clique[j]] 是否全为 true,全部相连才加入(保证团性质);③ sz 达到 S 时 ans++ 并回溯;④ 回溯时执行 sz-- 把点移出,重新标记为未使用,避免遗漏其他方案;⑤ 多组数据结束时清空邻接表、邻接矩阵与 ans。
2016思路题大连站 D · A Simple Math Problem
给定正整数 a、b,求 X + Y = a 且 lcm(X, Y) = b 的解(X ≤ Y)。请给出推导思路。
解析:设 gcd(X,Y) = g,则 X = g·k₁,Y = g·k₂ 且 k₁、k₂ 互质。方程改写为 k₁ + k₂ = a/g,k₁·k₂ = b/g,且说明 g 必为 gcd(a,b)。 于是构造一元二次方程 gx² − ax + b = 0:判别式 Δ = a² − 4·g·b; Δ < 0、√Δ 非整数、或 (a ± q) 为奇数时输出 No Solution;否则输出 (a−q)/2 与 (a+q)/2。 看到与最小公倍数相关的内容,往最大公约数方向转化是标准套路。
2017单项选择沈阳站 I · Little Boxes
求 a + b + c + d(每个数 ≤ 2⁶²)。用 unsigned long long 计算时,什么情况下需要特判?
答案 C。只有四个数都是 2⁶² 时和才达到 2⁶⁴ = 18446744073709551616,超出 unsigned long long 上界,此时特判直接输出这个数;其他情况正常用 unsigned long long 计算输出。数据溢出是竞赛中最常见的问题之一,要对数据范围保持敏感。
2017单项选择北京站 J · Pangu and Stones
盘古每次只能把连续的 L~R 堆石头合并成一堆(代价 = 石头总数),求合并成一堆的最短时间。DP 状态应定义为?
答案 B。需要第三维 k 记录区间内当前的堆数才能保证每次合并 L~R 堆的约束。转移分两步:先在区间内把 k 堆合并(k 从 L−1 取到 R−1),再整体并成一堆加上 sum;初始化 dp[i][j][j−i+1] = 0,答案 dp[1][N][1],不可达输出 0。处理区间时注意边界与初始化。
2017思路题北京站 E · Cats and Fish
m 条鱼 n 只猫,第 i 只猫吃一条鱼需 cᵢ 分钟,吃完立刻吃下一条;鱼不够时吃得快的猫优先;所有猫同时开始。求 x 分钟后剩多少条完整鱼、多少条吃了一部分的鱼。
解析:数据范围不大(m ≤ 5000,n ≤ 100,x ≤ 1000),直接模拟: 用优先队列存 pair(吃完当前鱼的时间, 吃一条鱼所需时长),小根堆按时间排序; 每次弹出最早吃完的猫:p(完整吃完数)++;若还没到时间 x 且鱼还有剩,就给它发新鱼(q++,重新入队)。 输出 m − p − q(剩下完整鱼)和 q(正在吃的鱼)。
易错点:注意输出的是「完整的鱼」和「吃了一部分鱼」两种;多组数据要清空优先队列
2018单项选择青岛站 A · Live Love
Live Love:给定长度可达 10⁶ 的仅含 0/1 的字符串,求最长连续 1 的段长。最合适的做法是?
答案 A。签到水题也考验复杂度意识:一遍扫描,遇 1 计数器加一、遇 0 清零并更新答案即可。看到 n = 10⁶ 就要本能地排除 O(n²)——与第 02 章复杂度体检器的结论一致。
2018单项选择南京站 J · Prime Game
Prime Game:答案为 Σ f(i,j)/j,其中 f(i,j) 是 a[i..j] 乘积的不同质因子个数。统计贡献时,质数 p 第 t 次出现在位置 pos_t(上一次出现在 pos_{t−1}),它负责的窗口左端点 i 的范围是?
答案 B。每个窗口的每个质因子只记一次,归给它最靠前的出现位置——即左端点落在 (pos_{t−1}, pos_t],右端点 j 可取 [pos_t, n]。这样不重不漏,配合 1/j 的前缀和即可 O(1) 完成每段贡献。
2018思路题南京站 J · Prime Game
请概述 Prime Game 的整体算法流程。
解析:① 预处理每个 aᵢ 的质因子;② 从左往右扫描,维护每个质数上一次出现的位置 prev[p]。扫到位置 i 的质数 p 时,它是窗口 [i′, j] 中 p 的首次出现当且仅当 i′ ∈ (prev[p], i]、j ∈ [i, n],这一段对答案的贡献为 (i − prev[p]) × Σ_{j=i}^{n} 1/j;③ Σ1/j 用分数形式的后缀和维护(分子分母分别累加,注意约分);④ 更新 prev[p] = i。整体复杂度约 O(n log A),避免了 O(n²) 枚举所有窗口。
2019单项选择上海站 B · Light bulbs
Light bulbs:一排灯泡初始全灭,n 的范围大到无法直接开数组,但只有 m ≤ 1000 次区间翻转操作 [l, r],求最终亮着的灯泡数。正确做法是?
答案 B。当 n 极大而操作数很少时,对操作端点离散化是标准套路:排序后相邻端点之间的整段覆盖次数相同,只需维护当前覆盖数,遇到端点时更新,奇数段长度累加进答案,复杂度 O(m log m)。
2019思路题上海站 B · Light bulbs
请写出端点扫描线的具体步骤。
解析:① 把 m 次操作 [l, r] 拆成事件 (l, +1) 与 (r+1, −1),共 2m 个端点;② 将端点升序排序,依次扫描相邻端点对 [x_k, x_{k+1}):该段内覆盖次数不变,若当前覆盖计数为奇数,则该段灯全亮,答案累加 x_{k+1} − x_k;③ 到达端点 x_{k+1} 时应用对应的事件(+1/−1)更新计数。注意 r+1 与题面的开闭区间约定保持一致即可。
2020单项选择考生回忆版(改编)
n 个数字,每次选取两个数合并成一个,代价为两数之和,求把所有数合并成一个的最小总代价。最优策略是?
答案 B。经典哈夫曼式贪心——每个数对总代价的贡献是「自身 × 参与求和的次数」,小数应尽早合并(被累加的次数多)、大数应尽量晚合并。用小根堆(priority_queue + greater)维护,每次取出最小两个、合并后放回,总复杂度 O(n log n)。
2020思路题考生回忆版(改编)
为什么「每次取最小两个合并」是正确的?请用交换论证简述。
解析:观察合并过程:一个数每参与一次合并,它的值就被加进总代价一次——越小的数应该被累加越多次。设最优解的第一步合并了 a、b,而全局最小的两个数是 x ≤ y。若 {x, y} 不是 {a, b},可以把 a、b 与 x、y 交换(x、y 更小,作为被合并项不会使后续代价更大),交换后总代价不增。反复交换,总能把最优解调整为「第一步合并最小两个」的形式,故贪心解不劣于最优解,即贪心即最优。这也是哈夫曼编码最优性的同款证明。
2021单项选择考生回忆版(改编)
把数组划分成 m 段,最小化「最大段和」。正确的算法框架是?
答案 B。「最大段和 ≤ x」的可行性随 x 增大单调变好 → 二分答案;判定用 O(n) 贪心:从左往右累加,超过 x 就开新段,统计段数是否 ≤ m。总复杂度 O(n log Σa)。二分边界:l = max(aᵢ),r = Σaᵢ。
2021思路题考生回忆版(改编)
二分答案适用的前提是什么?判定函数怎么写?
解析:前提是判定函数关于答案单调——若「存在方案使目标值 ≤ x」在 x = k 时成立,则 x > k 时也成立(或反之)。判定函数要能做到 O(n) 或 O(n log n):本题从左往右累加元素,一旦当前段和超过 x 就另起新段,最后比较段数与 m。此类「最小化最大值 / 最大化最小值」问题(跳石头、分巧克力等)都是同一模板,是阶段二必须练熟的题型。
2022单项选择考生回忆版(改编)
给定课程先修关系(有向图),判断能否修完全部课程。应使用?
答案 B。Kahn:统计入度 → 入度为 0 的点入队 → 出队并给后继入度 −1 → 统计出队总数;出队数 < n 说明剩余点互相依赖构成环。复杂度 O(n + m)。
2022思路题考生回忆版(改编)
写出 Kahn 拓扑排序的完整步骤,并说明为什么它能判环。
解析:① 建图并统计每个点的入度;② 所有入度为 0 的点入队;③ 出队 u,遍历 u 的每条出边 v:in[v]−−,若 in[v] 变为 0 则 v 入队;④ 记录出队总数。
判环原理:环上的每个点都有环内前驱,入度永远无法减到 0,因此永远不会入队——出队数 < n 即存在环。
扩展:把普通队列换成优先队列(小根堆),可以得到字典序最小的拓扑序;拓扑序也是 DAG 上 DP 的合法计算顺序。
2023单项选择考生回忆版(改编)
n 场活动各有起止时间,同一时刻只能参加一场,求最多能参加的活动数。贪心排序依据是?
答案 B。经典区间调度:按右端点升序排序,当前活动与已选不冲突就选。反例警示:按开始时间排序会被一个很长的活动卡死;按时长排序也可能被「跨越多个短活动」的长区间坑。正确性用交换论证证明——最早结束的活动总可以替换最优解中先选的那个活动。
2024单项选择题库精选(改编)
旅行商问题(TSP):n ≤ 20 个城市,求经过所有城市恰好一次并回到起点的最短路径。合适的复杂度是?
答案 C。状态 dp[S][i]:已访问集合为 S、当前在城市 i 的最短路径长。转移枚举上一个城市,状态数 n·2ⁿ、每个转移 O(n) → O(n²·2ⁿ)。n = 20 时约 4×10⁸ 次轻量运算,可以过——正好呼应第 02 章复杂度表「n ≤ 25 → O(2ⁿ)」这一行。
2024思路题题库精选(改编)
状压 DP 中如何高效枚举一个掩码 mask 的所有非空子集?
C++for (int s = mask; s; s = (s - 1) & mask) {
    /* s 依次取 mask 的每个非空子集,从大到小 */
}
/* 若还需包含空集,循环结束后单独处理 s = 0 */
解析:每次 s−1 把最低位的 1 借走,再与 mask 按位与,恰好跳到下一个更小的子集且不重不漏。总时间枚举所有掩码的所有子集为 O(3ⁿ)。这个技巧在子集卷积、枚举断点转移的 DP 中大量出现。
2025单项选择官方样题
n 个点分属若干集合,同集合内任意两点可互相直达、代价均为 tᵢ。若两两连边,边数会爆炸(ΣSᵢ 可达 10⁶)。正确的建图方式是?
答案 B。虚点建图把 O(Sᵢ²) 条边压缩为 O(Sᵢ) 条,总边数 O(ΣSᵢ)。之后从源点和汇点各跑一遍 Dijkstra,枚举相遇点取 max(d₁, dₙ) 的最小值即可(Meeting 类问题原型)——「建图是很关键的一步,有时建出来就会做了」。
2025思路题官方样题
用 01 字典树求「与给定 x 异或最大的已插入值」,请描述查询过程;若还需支持删除应如何改造?
解析:① 插入:每个数按二进制从最高位到最低位插入字典树,每位对应一个 0/1 分支;② 查询:从根出发逐位走——若与当前位相反的子节点存在且经过次数 > 0,走过去并给答案这一位或上 1(两位不同异或才为 1);否则只能走相同方向;③ 支持删除:每个节点加 cnt 记录经过次数,插入 +1、删除 −1,cnt 为 0 的节点视作不存在(Chip Factory 类问题的标准改造)。
2026单项选择模拟预测
树状数组的核心函数 lowbit(x) 的正确实现是?
答案 A。x & (−x) 取出 x 二进制最低位的 1 及其后的 0,正是树状数组父子下标跳转的依据:update 用 x += lowbit(x),query 用 x -= lowbit(x)。注意下标必须从 1 开始,出现 0 会让 lowbit 死循环。
2026思路题模拟预测
用树状数组求逆序对数量,请给出完整思路与复杂度。
解析:① 值域大时先离散化(排序 + lower_bound 映射到 1..n);② 从右往左扫描:对每个 aᵢ,先查询已插入的数中严格小于 aᵢ 的个数——ans += query(aᵢ − 1)(这些数在 aᵢ 右侧且比它小,构成逆序对),再把 aᵢ 插入 update(aᵢ, 1);③ 累加即为逆序对总数,复杂度 O(n log n)
对照:归并排序求逆序对复杂度相同(合并时统计跨越左右两半的逆序),两种写法都要会。